/*
 * Copyright 2012 Alain Pannetrat (L1L1@gmx.com)
 * 
 * This file is part of cookie-miner.
 * 
 * cookie-miner is free software: you can redistribute it and/or modify 
 * it under the terms of the GNU General Public License as published by 
 * the Free Software Foundation, either version 3 of the License, or 
 * (at your option) any later version.
 * 
 * cookie-miner is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License 
 * along with cookie-miner. If not, see http://www.gnu.org/licenses/.
 *
 * @version: $Id$
 *
 */
#include "http_server.h"
#include <dirent.h>
#include <sys/types.h> 
#include <sys/stat.h> 
#include <unistd.h>
#include <fcntl.h>

class HttpServerInstance : private Uncopyable {
	public:
		HttpServerInstance(HttpServer& server, int socket);
		
		bool run();

		~HttpServerInstance();
	private:
		HttpServer&		_server;
		BufferInOutSocket 	_bio;
};

/********************************************************************
 *
 *	MIME TYPES
 *
 */

#include "mime_types.inc"

const char *guess_mime_type(const std::string& filename)
{
	size_t pos = filename.rfind('.');
	int min,max,mid,comp;

	if (pos == std::string::npos) return NULL;

	min = 0;
	max = sizeof(MIME_TYPES)/sizeof(mime_type_t);
	while (min<=max)
	{
		mid = (min+max)/2;
		comp = strcasecmp(MIME_TYPES[mid].extension,filename.c_str()+pos+1);
		if (comp<0) 
			min = mid+1;
		else if (comp>0)
			max = mid-1;
		else
			return MIME_TYPES[mid].mimetype;
	}
	return NULL;
}

/********************************************************************
 *
 * HTTP_SERVER
 *
 */

class thread_context_t : private Uncopyable {
	public:
		thread_context_t(HttpServer& server, int socket) : 
			thread_server(server), thread_socket(socket) {}    
		HttpServer& 	thread_server;
		int 		thread_socket;
};

void *server_instance_thread(void *ctx_ptr)
{
	thread_context_t *ctx = (thread_context_t *)ctx_ptr;
	HttpServerInstance server(ctx->thread_server, ctx->thread_socket);

	logger.new_thread();
	logger.message(logger.LOG_DEBUG,"Creating server thread with socket %i.",ctx->thread_socket);

	server.run();

	tcp_close(ctx->thread_socket);
	logger.message(logger.LOG_DEBUG,"Exiting server thread.");
	delete ctx;
	pthread_exit(NULL);
	return NULL;
}

HttpServer::HttpServer(Json::Value *config)
{
	if (config->get("server")->get("root")->is_defined())
		strncpy(_server_root,config->get("server")->get("root")->as_string().c_str(),PATH_MAX);
	else
		getcwd(_server_root,PATH_MAX);
	if (config->get("server")->get("port")->is_defined())
		_server_port = config->get("server")->get("port")->as_int();
	else
		_server_port = 8081;
}

HttpServer::HttpServer(unsigned short port, const char *root_path)
{
	if (root_path)
		strncpy(_server_root,root_path,PATH_MAX);
	else
		getcwd(_server_root,PATH_MAX);

	_server_port = 8081;
}


HttpServer::~HttpServer() 
{

}

bool HttpServer::run()
{
    char ip[20];
    int sock;
    pthread_t thread_ref;

    if ((sock = tcp_listen(_server_port))<0)
	return false;

    for (;;)
    {
	thread_context_t ctx(*this, tcp_accept(sock,ip));

	pthread_create(&thread_ref,NULL,&server_instance_thread,&ctx);
	pthread_detach(thread_ref);
    }
}

bool HttpServer::do_default_error(BufferInOutSocket& bio, 
				  int errcode, 
				  bool include_content, 
				  const std::string& extra_content) const
{
    HttpPacket response;

    switch (errcode) {
	case 400: 
	    response.headline().set("HTTP/1.1 400 Bad Request\r\n");
	    break;
	case 401:
	    response.headline().set("HTTP/1.1 401 Unauthorized\r\n");
	    break;
	case 402:
	    response.headline().set("HTTP/1.1 402 Payment Required\r\n");
	    break;
	case 403:
	    response.headline().set("HTTP/1.1 403 Forbidden\r\n");
	    break;
	case 404:
	    response.headline().set("HTTP/1.1 404 Not Found\r\n");
	    break;
	case 405:
	    response.headline().set("HTTP/1.1 405 Method Not Allowed\r\n");
	    break;
	case 500:
	    response.headline().set("HTTP/1.1 500 Internal Server Error\r\n");
	    break;
    }

    response.headers().set("Server",server_name());
    response.content_add("<html><body><h1>" + response.headline().full() + "</h1>" + extra_content + "</body></html>");
    response.headers().set("Content-length",response.content_length());

    if (!response.headline().write_out(&bio) ||
	!response.headers().write_out(&bio) ||
	!(include_content?response.content_write_out(&bio):true) ||
	!bio.write_flush())
    {
	logger.message(logger.LOG_ERROR,"Failed to send ERROR %i response packet to client",errcode);
	return false;
    }
    logger.message(logger.LOG_ERROR,"returned %s",response.headline().full().c_str());
    return true;	
}

#ifdef __WIN32__
#define realpath(N,R) _fullpath((R),(N),_MAX_PATH)
#endif

bool normalize_path(std::string &rpath, const std::string& server_root, const std::string& path)
{
    char path_cstr[MAX_PATH];
    char *last_char;

    rpath = server_root + path;

    realpath(rpath.c_str(),path_cstr);

    last_char = path_cstr + strlen(path_cstr) - 1;

    while (last_char>path_cstr && (*last_char=='\\' || *last_char=='/'))
    {
	*last_char=0;
	last_char--;
    }

    if (*last_char==':') // FIXME: possible buffer overflow
    {
	last_char[1]='/';
	last_char[2]=0;
    }

    rpath = path_cstr;

    return true;
}

bool HttpServer::do_default_content(BufferInOutSocket& bio, 
				    const HttpPacket& request,	
				    bool include_content) const
{
        URL url(request.headline().part(1));
	HttpPacket response;
	struct dirent *de;
	DIR *cdir;
	std::string http_filename;
	std::string real_filename;
	std::string real_basename;
	std::string candidate;
	struct stat st;
	struct stat st_candidate;
	unsigned pos;

	http_filename = url.path();

	if (http_filename == "/debug")
	{
		KeyValueList::const_iterator it;
		
		response.headline().set("HTTP/1.1 200 OK\r\n");
		response.headers().set("Server",server_name());
		response.headers().set("Content-Type","text/html");
		response.content_add("<html>\r\n<head></head>\r\n<body style='font-family: monospace;'>\r\n<h1>Debug information</h1>\r\n");

		response.content_add("<h2>* REQUEST:</h2>\n");
		response.content_add(url.encode_relative());

		response.content_add("<h2>* HEADERS:</h2>\n");
		for (it=request.headers().begin();it!=request.headers().end();++it)
			response.content_add("<b>" + it->first + ": </b>" + it->second + "<br />\n");

		response.content_add("<h2>* QUERY_STRING:</h2>\n");
		for (it=url.query_string_begin();it!=url.query_string_end();++it)
			response.content_add("<b>" + it->first + "</b>=" + it->second + "<br />\n");


		response.content_add("</body>\r\n");
		response.headers().set("Content-length",response.content_length());

		if (!response.headline().write_out(&bio) ||
				!response.headers().write_out(&bio) ||
				!(include_content?response.content_write_out(&bio):true) ||
				!bio.write_flush())
		{
			logger.message(logger.LOG_ERROR,"Failed to send response packet to client");
			return false;
		}
		return true;	
	}

	if (!normalize_path(real_filename, root(), http_filename))
		return do_default_error(bio, 404, include_content, http_filename + " malformed.");

	if (stat(real_filename.c_str(),&st)!=0)
		return do_default_error(bio, 404, include_content, http_filename + ": " + strerror(errno));
	
	if (S_ISDIR(st.st_mode))
	{
		std::string candidate = real_filename + "/index.html";

		if (stat(candidate.c_str(),&st_candidate)==0)
		{
			real_filename = candidate;
			st = st_candidate;	
		}
	}

	if ((pos = real_filename.find_last_of("/\\"))!=std::string::npos)
		real_basename = real_filename.substr(pos+1);
	else
		real_basename = real_filename;

	if (S_ISDIR(st.st_mode))
	{
		if ((cdir = opendir(real_filename.c_str()))==NULL)  
			return do_default_error(bio,404,include_content,"Could not open dir " + real_filename);

		response.headline().set("HTTP/1.1 200 OK\r\n");
		response.headers().set("Server",server_name());
		response.headers().set("Content-Type","text/html");
		response.content_add("<html>\r\n  <head></head>\r\n"
				     "<body style='font-family: monospace;'>\r\n"
				     "<p><b>" + url.path() + "</b></p><hr /><ul>\r\n");

		response.content_add("<li><a href=\"..\">[PARENT]</a></li>\r\n");

		while ((de = readdir(cdir)))
		{
			if (de->d_name[0]!='.')
			{
				candidate = real_filename + "/" + de->d_name;

				stat(candidate.c_str(),&st_candidate);

				if (S_ISDIR(st_candidate.st_mode))
				{	
					response.content_add("<li><a href=\"" + 
							URL::entity_encode(de->d_name) +	
							"/\">" + de->d_name + "/</a></li>\r\n");
				}
				else
				{
					response.content_add("<li><a href=\"" + 
							URL::entity_encode(de->d_name) +	
							"\">" + de->d_name + "</a></li>\r\n");
				}
			}
		}
		response.content_add("</ul></body>\r\n</html>\r\n");
		response.headers().set("Content-length",response.content_length());

		if (!response.headline().write_out(&bio) ||
				!response.headers().write_out(&bio) ||
				!(include_content?response.content_write_out(&bio):true) ||
				!bio.write_flush())
		{
			logger.message(logger.LOG_ERROR,"Failed to send response packet to client");
			return false;
		}
		return true;	

	}

	if (S_ISREG(st.st_mode))
	{
		const char *mime_type = guess_mime_type(real_filename);
		int fd = -1;

		if (include_content)
		{
			fd = open(real_filename.c_str(),O_RDONLY|O_BINARY);
			if (fd<0) return do_default_error(bio,403,include_content,strerror(errno));
		}

		response.headline().set("HTTP/1.1 200 OK\r\n");
		response.headers().set("Server",server_name());
		if (mime_type) response.headers().set("Content-Type",mime_type);
		response.headers().set("Content-length",st.st_size);
		if (!response.headline().write_out(&bio) ||
				!response.headers().write_out(&bio))
		{
			logger.message(logger.LOG_ERROR,"Failed to send response packet to client");
			if (include_content) close(fd);
			//delete response;
			return false;
		}

		if (include_content)
		{
			unsigned char buf[8192];
			ssize_t sz,rd;
			unsigned wr;

			sz=0;
			while (sz<st.st_size)
			{
				rd = read(fd,buf,8192);
				if (rd<=0) break;
				wr = rd;
				bio.write_block(wr,buf);
				sz+=rd;
			}
			bio.write_flush();
			close(fd);
			logger.message(logger.LOG_DEBUG,"returned %u bytes of content (%s)",sz,mime_type?mime_type:"no type");
		}

		//delete response;
		return true;
	}

	return do_default_error(bio,404,include_content, http_filename + " is not a file or dir.");
}


bool HttpServer::do_get(BufferInOutSocket& bio, const HttpPacket& request)
{
    return do_default_content(bio,request,true);
}

bool HttpServer::do_head(BufferInOutSocket& bio, const HttpPacket& request)
{
    return do_default_content(bio,request,false);
}

bool HttpServer::send_error(BufferInOutSocket& bio, int error) const
{
    return do_default_error(bio,error,true,"");
}

bool HttpServer::dispatch_request(BufferInOutSocket& bio, const HttpPacket& request)
{
    if (request.headline().part(0)=="GET") return do_get(bio,request);

    if (request.headline().part(0)=="HEAD") return do_head(bio,request);

    return send_error(bio,405);
}

/********************************************************************
 *
 * 	HTTP_SERVER_INSTANCE
 *
 */

HttpServerInstance::HttpServerInstance(HttpServer& server, int socket) :
			_server(server),
			_bio(socket)
{ 
    /* void */
}

HttpServerInstance::~HttpServerInstance()
{
    /* void */
}

bool HttpServerInstance::run()
{
    bool running = true;

    while (running)
    {
	HttpPacket packet;

	if (!packet.headline().read_in(&_bio))
	    running = false;
	else 
	{
	    logger.message(logger.LOG_DEBUG,"Request: %s",packet.headline().full().c_str());

	    if (!packet.headers().read_in(&_bio))
		running = _server.send_error(_bio,400);
	    else 
		running = _server.dispatch_request(_bio,packet);
	}

	if (packet.headers().match_word("Connection","close")!=packet.headers().end() || _bio.read_end())
	    running = false;

    }
    return true; 
}

/*
int main()
{
	HttpServer server;

	server.run();	
}
*/
